Conversation
Greptile SummaryThis PR replaces the hard-coded Key changes:
Confidence Score: 5/5Safe to merge — the change is a clean drop-in replacement with no correctness issues. No P0 or P1 issues found. The new No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["useLayoutEffect fires\n(wrapLines changed)"] --> B{anchor found?}
B -- no --> C[update prev refs]
B -- yes --> D["restoreViewportAnchor()\n(immediate, synchronous)"]
D --> E["suppressNextSelectionAutoScroll = true"]
E --> F["schedulePaintRetries(restoreViewportAnchor, 2)"]
F --> G["scheduleNext(2)\n→ push cancel1"]
G --> H["Frame 1 (rAF / setTimeout 16ms)\ncallback() + scheduleNext(1)\n→ push cancel2"]
H --> I["Frame 2 (rAF / setTimeout 16ms)\ncallback()"]
F --> J["return cancelFn\n(cancels.splice(0).forEach)"]
J --> K["Effect cleanup\ncancels all pending frames"]
|
| function schedulePaintRetries(callback: () => void, frames = 2) { | ||
| const cancels: Array<() => void> = []; | ||
| const requestFrame = | ||
| typeof globalThis.requestAnimationFrame === "function" | ||
| ? (next: () => void) => { | ||
| const frameId = globalThis.requestAnimationFrame(() => next()); | ||
| return () => { | ||
| if (typeof globalThis.cancelAnimationFrame === "function") { | ||
| globalThis.cancelAnimationFrame(frameId); | ||
| } | ||
| }; | ||
| } | ||
| : (next: () => void) => { | ||
| const timeout = setTimeout(next, 16); | ||
| return () => clearTimeout(timeout); | ||
| }; | ||
|
|
||
| const scheduleNext = (remaining: number) => { | ||
| const cancel = requestFrame(() => { | ||
| callback(); | ||
| if (remaining > 1) { | ||
| scheduleNext(remaining - 1); | ||
| } | ||
| }); | ||
| cancels.push(cancel); | ||
| }; | ||
|
|
||
| scheduleNext(frames); | ||
| return () => { | ||
| cancels.splice(0).forEach((cancel) => cancel()); | ||
| }; |
There was a problem hiding this comment.
Stale cancel handles accumulate in the
cancels array
After a frame fires, its cancel handle stays in the cancels array even though calling cancelAnimationFrame on a frame that has already executed is a harmless no-op. With frames=2:
scheduleNext(2)→cancels = [cancel1]- Frame 1 fires →
scheduleNext(1)→cancels = [cancel1, cancel2](cancel1 is now stale) - Frame 2 fires →
cancels = [cancel1, cancel2](both stale if cleanup runs here)
The cleanup calls both, but cancel1 and cancel2 become no-ops once their respective frames have fired. This is correct in all paths but results in unnecessary no-op calls. A minor defensive improvement would be to splice the cancel out of the array once its frame has run:
const scheduleNext = (remaining: number) => {
const cancel = requestFrame(() => {
const idx = cancels.indexOf(cancel);
if (idx !== -1) cancels.splice(idx, 1);
callback();
if (remaining > 1) {
scheduleNext(remaining - 1);
}
});
cancels.push(cancel);
};This is low priority since cancelAnimationFrame/clearTimeout on already-fired IDs is well-defined as a no-op in all major environments.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
requestAnimationFrameand fall back to timed frames in testsTesting
bun run typecheckbun testbun run test:tty-smoketimeout 5 script -q -f -e -c "bun run src/main.tsx -- diff <before> <after>" <transcript>This PR description was generated by Pi using OpenAI GPT-5